/* App State */
class AppState {
	constructor() {
		this.version = null;
		try {
			const manifest = chrome.runtime.getManifest();
			this.version = manifest?.version;
		} catch (e) {
			console.error(e);
		}
		this.state = { // persisted state
			settings: {}, // Extension specific settings, User settings are managed in web app.
			thread_sort_mode: 'date_desc', // default to newest sort mode
		};

		// settings
		this.settingsDefault = {
			server_url: "https://solfray.com", // fallback server url
			server_api_key: '',
			font_size: '0.7em',
		};
		this.settingsSchema = {
			server_url: 'string',
			server_api_key: 'string',
			font_size: 'string',
		};
		this.settingsDescriptions = {
			server_url: 'The URL of the server to send chats to.',
			server_api_key: 'Your API key for interfacing with the server.',
			font_size: 'General font size of extension text. Some elements may be larger or smaller.',
		};
		for (let key in this.settingsDefault) this.settingsSchema[key] = typeof this.settingsDefault[key];
		this.settingsSchema.server_url = 'string';

		// other session state variables (not persisted)
		this.publicKey = null; // fetched from server using api key
		this.lockedThreadId = null; // set to id if locked
		this.currentURL = null; // The current URL being viewed by the user.
		this.myChannels = null; // Fetched from /auth/me endpoint
		this.userFiat = null; // Fetched from /auth/me endpoint
		this.loadState();

		// Uses class CUN2 from thread.js
		this.threadApp = null; // set dynamically when a thread is loaded
	}

	getCurrentURL() {
		return this.currentURL || null;
	}

	setCurrentURL(url) {
		this.currentURL = null;
		$('#thread-container').empty();
		if (typeof url == 'string' && url.length > 0 && url.toLowerCase().startsWith('https://')) {
			this.currentURL = url;
			// get site base url for tree opener. Remove all queries and paths.
			const urlObj = new URL(url);
			const baseURL = `${urlObj.protocol}//${urlObj.hostname}`;
			const colDiv = $(`<div class="col-md-8 offset-md-2"></div>`);
			const treeOpener = $(`<a href="#" class="tree_opener" data-base-url="${baseURL}" title="List all URLs with threads at this site."><span class="bi bi-diagram-2"></span></a>`);
			treeOpener.on('click', (e) => {
				const siteBaseURL = $(e.currentTarget).data('base-url') || null;
				const serverURL = this.getSetting('server_url') || null;
				const slash = serverURL.endsWith('/') ? '' : '/';
				const apiKey = this.getSetting('server_api_key') || null;
				if(!serverURL || typeof serverURL != 'string' || serverURL.length < 1){
					this.feed("Set Server URL in extension settings.", true);
					return;
				}
				if(!apiKey || typeof apiKey != 'string' || apiKey.length < 1){
					this.feed("Set API Key in extension settings.", true);
					return;
				}
				$('#reply-container').empty().append('<span>Loading site thread tree...</span>');
				fetch(`${serverURL}${slash}api/site_tree?base_url=${encodeURIComponent(siteBaseURL)}`, {
					headers: {
						'Authorization': `Bearer ${apiKey}`
					}
				}).then(
					response => response.json()
				).then(data => {
					if (data && data.ok) {
						const closeIcon = bsi('x-lg') || '❌';
						const closeLink = $(`<a href="#" class="btn btn-sm btn-secondary float-end" id="close_tree">${closeIcon} Close</a>`);
						closeLink.on('click', e => {
							e.preventDefault();
							$('#reply-container').empty();
							this.getThreads();
						});
						const treeSearch = $(`
							<input type="text" id="tree_search_input" class="form-control" placeholder="Search URLs..." title="Type to filter URLs in the table below.">
						`);
						treeSearch.on('input', (e) => {
							const searchTerm = $(e.currentTarget).val().toLowerCase();
							if (!searchTerm || searchTerm.length < 1) {
								// show all rows
								$('#tree_table tbody tr').show();
								return;
							}
							$('#tree_table tbody tr').each((i, row) => {
								var urlCell = $(row).find('td:nth-child(2)').text().toLowerCase();
								// replace all dashes, underscores, and spaces with single space for matching
								urlCell = urlCell.replace(/[-_]+/g, ' ').replace(/\s+/g, ' ');
								if (urlCell.indexOf(searchTerm) > -1) {
									$(row).show();
								} else {
									$(row).hide();
								}
							});
						});
						const treeTable = $(`
							<table id="tree_table" class="table table-sm table-bordered">
								<thead>
									<tr>
										<th title="Thread Count">No.</th>
										<th title="Page URL">Page URL</th>
									</tr>
								</thead>
								<tbody></tbody>
							</table>
						`);
						if (Array.isArray(data.thread_counts) && data.thread_counts.length > 0) {
							for (let i = 0; i < data.thread_counts.length; i++) {
								const row = data.thread_counts[i];
								const tr = $(`
									<tr>
										<td title="Number of threads at this URL" style="text-align: center; vertical-align: middle;">${row.thread_count || 0}</td>
										<td><a href="${row.url || ''}" target="_blank">${row.url || ''}</a></td>
									</tr>
								`);
								treeTable.find('tbody').append(tr);
							}
						} else {
							treeTable.find('tbody').append('<tr><td colspan="2">No threads found at this site.</td></tr>');
						}
						$('#reply-container').empty().append(closeLink, '<br><br>', treeSearch, '<br><br>', treeTable);
					} else {
						this.feed("Failed to load site thread tree.", true);
					}
				}).catch(err => {
					console.error(err);
					this.feed("Error loading site thread tree.", true);
				});
			});
			colDiv.append(treeOpener, `&nbsp;<span class="text-muted small" title="${this.currentURL}">${this.getShortURL()}</span>`);
			$('#thread-container').append(colDiv);
			setTimeout(render_bsi, 50);
		}
		return this.currentURL;
	}

	feed(arg, err = false) {
		if (err){
			console.trace(arg);	// for debugging
			$('#feed_error').removeClass('d-none');
		}else{
			$('#feed_error').addClass('d-none');
		}
		$('#feed').empty().append((arg.toString() || "&nbsp;"));
	}

	lockThread() {
		// Will prevent re-render on user navigation in browser.
		this.lockedThreadId = (this.threadApp && typeof this.threadApp == 'object')? (this.threadApp?.thread_id || null): null;
		return this.lockedThreadId ? true : false;
	}

	unlockThread() {
		const wasLocked = this.lockedThreadId ? true : false;
		// Will allow re-render on user navigation in browser.
		this.lockedThreadId = null;
		if(wasLocked) this.getThreads();
		return false;
	}

	toggleThreadLock() {
		if (this.lockedThreadId) return this.unlockThread();
		return this.lockThread();
	}

	getShortURL() {
		const url_len = 40;
		var url = this.currentURL || null;
		if (!url || typeof url !== 'string') return null;
		url = url.replace(/^https:\/\/(www\.)?/, '');
		var shortUrl = url.substring(0, url_len);
		return url.length > url_len ? shortUrl + "..." : url + "";
	}

	primeThread(chatId, replyToChatId = null, exitChatId = null) {
		console.log('not implemented yet');
		return;
	}

	emptyContainers() {
		// clean slate
		$('#thread-container').empty();
		$('#reply-container').empty();
		$('#compose_container').empty();
	}

	unloadThread() {
		// destroy existing threadApp if exists
		if (this.threadApp && typeof this.threadApp == 'object') {
			this.threadApp.destroy();
			this.threadApp = null;
		}
		$('#thread-container').removeClass('thread');
		// render short url display
		this.setCurrentURL(this.getCurrentURL()); 
	}

	loadThread(chatId, authorPK = null) {
		$("#thread_id").val(chatId);
		this.emptyContainers();
		this.unloadThread();
		$('#thread-container').addClass('thread'); // required by render_threads().
		this.threadApp = new CUN2(chatId, authorPK, this.publicKey, this.getSetting('server_url'));
		this.threadApp.start((e) => { // form submit handler
			e.preventDefault();
			// serialize form
			const formDataArray = $(e.target).serializeArray();
			const dict = {};
			formDataArray.forEach(item => {
				dict[item.name] = item.value;
			});
			// Empty the form inputs
			$(e.target).find('input[type="text"], textarea, number').val('');
			// Validate and process form data
			const lamports = parseInt(dict?.['tip_amount_lamports'] || '0') || 0;
			const serverURL = this.getSetting('server_url') || null;
			if (!serverURL || typeof serverURL != 'string' || serverURL.length < 1) {
				alert("Set Server URL in extension settings.");
				return;
			}
			const apiKey = this.getSetting('server_api_key') || null;
			if (!apiKey || typeof apiKey != 'string' || apiKey.length < 1) {
				alert("Set Server API Key in extension settings.");
				return;
			}
			const slash = serverURL.endsWith('/') ? '' : '/';
			dict.top_chat_id = $("#thread_id").val();
			if(lamports > 0){
				// POST the dict to /api_tip_target for user to sign and send tip in web app.
				const endpoint = `${serverURL}${slash}api_tip_target`;
				var formInputs = '';
				for(const key in dict){
					const val = dict[key];
					formInputs += `<input type="hidden" name="${key}" value="${val}">`;
				}
				const postForm = $(`
					<form method="post" action="${endpoint}" target="_blank">
						<input type="hidden" name="api_key" value="${apiKey}">
						${formInputs}
					</form>
				`);
				// Lock thread before submission
				this.lockThread();
				// Submit form
				$('body').append(postForm);
				postForm.submit();
				// Remove form after submission
				postForm.remove();
				return;
			}else{ // Users can send messages without tip via extension
				const endpoint = `${serverURL}${slash}api/chats/create`;
                $.ajax({
                    url: endpoint,
                    method: "POST",
					headers: {
						'Content-Type': 'application/json',
						'Authorization': `Bearer ${apiKey}`
					},
                    data: JSON.stringify(dict),
                    contentType: "application/json",
                    success: (data) => {
                        if (data.ok) {
                            this.threadApp.build_composer_form();
                            this.threadApp.load_replies();
                        } else {
                            alert("Failed to post reply: " + data.error?.message || "Unknown error");
                        }
                    },
                    error: (xhr, status, error) => {
                        console.error("Failed to post reply:", xhr, status, error);
                        this.threadApp.end_session();
                    }
                });
			}
		});
		this.threadApp.build_composer_form();
	}

	getThreads(url = null) {
		if (this.lockedThreadId) return; // User does not want auto reloading on navigation.
		if(url){ 
			// url passed means first time load for specific url, check if redundant
			// nuances of browser navigation may cause redundant calls
			const currentURL = this.getCurrentURL();
			if(currentURL && currentURL === url){
				return; // redundant, do nothing
			}
		} // else, no url passed, means reload for current url
		this.emptyContainers();
		this.unloadThread();
		if (url) this.setCurrentURL(url); // will ignore non-https urls
		url = this.getCurrentURL();
		// If the url is about blank or similar, ignore it
		if(
			url && (
				url.toLowerCase() === 'about:blank' || 
				url.toLowerCase().startsWith('chrome://') || 
				url.toLowerCase().startsWith('edge://') || 
				url.toLowerCase().startsWith('brave://') || 
				url.toLowerCase().startsWith('opera://') || 
				url.toLowerCase().startsWith('safari://') || 
				url.toLowerCase().startsWith('moz-extension://') || 
				url.toLowerCase().startsWith('chrome-extension://') || 
				url.toLowerCase().startsWith('file://') || 
				url.toLowerCase().startsWith('view-source:') ||  
				url.toLowerCase().startsWith('data:')
			)
		){
			// Empty containers to signal to user that this url is unsupported
			this.emptyContainers();
			// destroy any loaded thread
			this.unloadThread();
			return;
		}
		const apiKey = this.getSetting('server_api_key') || null;
		const serverURL = this.getSetting('server_url') || null;
		if(!serverURL || typeof serverURL != 'string' || serverURL.length < 1){
			this.feed("Set Server URL in extension settings.", true);
			return;
		}
		if(!apiKey || typeof apiKey != 'string' || apiKey.length < 1){
			this.feed("Set Server API Key in extension settings.", true);
			return;
		}
		// Try to fetch threads using GET request to server + Authorization header
		try{
			const slash = serverURL.endsWith('/') ? '' : '/';
			const endpoint = `${serverURL}${slash}api/threads_at_url?url=${encodeURIComponent(url)}`;
			fetch(endpoint, {
				method: 'GET',
				headers: {
					'Content-Type': 'application/json',
					'Authorization': `Bearer ${apiKey}`
				}
			}).then(response => {
				if(!response.ok) {
					response.text().then(text => {
						console.error('Fetch response not ok text:', text);
					});
					throw new Error(`Server responded with status ${response.status}`);
				}
				return response.json();
			}).then(data => {
				$('#reply-container').empty();
				if(!data || typeof data != 'object' || !Array.isArray(data.threads)){
					this.feed("Invalid response from server.", true);
					return;
				}
				const threadCount = data.threads.length;
				if(threadCount > 1){		
					const threadSearch = $(`
						<input type="text" id="thread_search_input" class="form-control" placeholder="Search threads...">
					`);
					threadSearch.on('input', (e) => {
						const searchTerm = $(e.currentTarget).val().toLowerCase().trim();
						if (!searchTerm || searchTerm.length < 1) {
							// show all threads
							$('.thread').show();
						} else {
							$('.thread').each((i, threadDiv) => {
								var threadTxt = $(threadDiv).text().toLowerCase();
								// replace all dashes, underscores, and spaces with single space for matching
								threadTxt = threadTxt.replace(/[-_]+/g, ' ').replace(/\s+/g, ' ');
								if (threadTxt.includes(searchTerm)) {
									$(threadDiv).show();
								} else {
									$(threadDiv).hide();
								}
							});
						}
					});
					$('#reply-container').append(threadSearch);
				}
				this.feed(`${threadCount} thread${threadCount != 1 ? 's' : ''} at this URL.`);
				for(let i=0; i<data.threads.length; i++){
					const thread = data.threads[i];
					$('#reply-container').append(`
						<div class="thread-container thread" 
							data-chat-id="${thread.chat_id}"
							data-author-alias="${(thread?.author_alias || '')}" 
							data-author-pic="${(thread?.author_pic || '')}"
							data-author-public-key="${(thread?.author_public_key || '')}"
							data-author-display-name="${(thread?.author_display_name || '')}" 
							data-content="${(thread?.content || '')}"
							data-url="${(thread?.url || '')}" 
							data-channel="${(thread?.channel_name || '')}"
							data-datetime-created="${(thread?.datetime_created || '').replace('+00:00', 'Z')}"
							data-reply-count="${(thread?.reply_count || 0)}">
						</div>
					`);
				}
				$('#composer-container').empty();
				var channelSelector = '';
				if(Array.isArray(this.myChannels) && this.myChannels.length > 0){
					try{
						channelSelector += `<div class="input-group" id="thread_channel_selector_container">`;
						channelSelector += `<label id="thread_channel_label" for="thread_channel" class="input-group-text" title="Select channel to post thread in.">Channel</label>`;
						channelSelector += `<select id="thread_channel" name="channel_id" class="form-control input-sm" title="Select channel to post thread in.">`;
						channelSelector += `<option value="">-- No Channel --</option>`;
						for(const ch of this.myChannels){
							const channelType = ch?.channel_type? ` - ${ch.channel_type}` : '';
							const selected = (this.state.last_channel_id && this.state.last_channel_id === ch.channel_id) ? ' selected' : '';
							channelSelector += `<option value="${ch.channel_id}"${selected}>${ch.name}${channelType}</option>`;
						}
						channelSelector += `</select>`;
						channelSelector += `</div>`;
					}catch(e){
						console.error('Error building channel selector:', e);
					}
				}
				const createThreadForm = $(`
					<form id="create_thread_form">
						${channelSelector}
						<div class="input-group" id="thread_form_input_group">
							<input type="hidden" id="thread_url" name="url" value="${url}">
							<input type="text" class="form-control thread_content" id="thread_content" name="content" placeholder="Create Thread" required="required" maxlength="280" autocomplete="off" spellcheck="true">
							<button id="trd_btn" type="submit" class="btn btn-primary form-control" title="Create New Thread">
								<span class="bi bi-plus-lg"></span>
							</button>
						</div>
					</form>`);
				createThreadForm.on('submit', (e) => {
					e.preventDefault();
					const formDataArray = $(e.target).serializeArray();
					const dict = {};
					formDataArray.forEach(item => {
						dict[item.name] = item.value;
					});
					const serverURL = this.getSetting('server_url') || null;
					if (!serverURL || typeof serverURL != 'string' || serverURL.length < 1) {
						this.feed("Set Server URL in extension settings.", true);
						return;
					}
					const apiKey = this.getSetting('server_api_key') || null;
					if (!apiKey || typeof apiKey != 'string' || apiKey.length < 1) {
						this.feed("Set Server API Key in extension settings.", true);
						return;
					}
					const slash = serverURL.endsWith('/') ? '' : '/';
					const endpoint = `${serverURL}${slash}api/chats/create`;
					$.ajax({
						url: endpoint,
						method: "POST",
						headers: {
							'Content-Type': 'application/json',
							'Authorization': `Bearer ${apiKey}`
						},
						data: JSON.stringify(dict),
						contentType: "application/json",
						success: (data) => {
							if (data.ok) {
								this.feed("Thread created successfully.");
								this.state.last_channel_id = dict.channel_id || null;
								this.saveState();
								this.getThreads();
							}else{
								this.feed("Failed to create thread: " + data.error?.message || "Unknown error", true);
							}
						}
					});
				});
				$('#composer-container').append(createThreadForm);
				setTimeout(()=>{
					render_bsi();
					render_threads(serverURL);
				},50);
			}).catch(error => {
				console.error('Error fetching threads:', error);
				this.feed("Error fetching threads from server.", true);
			});
		}catch(e){
			this.feed("Error preparing to fetch threads from server.", true);
			$('#thread-container').empty();
			return;
		}
	}

	authenticate(){
		// Attempt to fetch my public key from the server (associated with API key)
		try{
			const serverURL = this.getSetting('server_url') || null;
			const apiKey = this.getSetting('server_api_key') || null;
			if (!serverURL || typeof serverURL != 'string' || serverURL.length < 1) return;
			if (!apiKey || typeof apiKey != 'string' || apiKey.length < 1) return;
			const slash = serverURL.endsWith('/') ? '' : '/';
			const endpoint = `${serverURL}${slash}auth/me`;
			fetch(endpoint, {
				method: 'GET',
				headers: {
					'Content-Type': 'application/json',
					'Authorization': `Bearer ${apiKey}`
				}
			}).then(response => {
				if (!response.ok) {
					throw new Error(`Server responded with status ${response.status}`);
				}
				return response.json();
			}).then(data => {
				if(!data || typeof data != 'object'){
					throw new Error('Invalid response data');
				}
				if(!data.ok || !data.authenticated){
					throw new Error('Authentication failed');
				}
				if(!data.public_key || typeof data.public_key != 'string'){
					throw new Error('No public key in response');
				}
				this.publicKey = data.public_key;
				this.userFiat = data?.user_fiat || null;
				if(this.userFiat && typeof this.userFiat == 'string'){
					$('#user_preferred_fiat').val(this.userFiat);
				}else{
					$('#user_preferred_fiat').val('');
				}
				this.myChannels = Array.isArray(data?.channels) ? data.channels : null;
			}).catch(error => {
				console.error('Error fetching user public key using api key. ', error);
			});
		}catch(e){
			console.error('Error fetching user public key using api key. ', e);
		}
	}

	checkForNotifications() {
		// Fetch count of unread notifications from server
		try {
			const serverURL = this.getSetting('server_url') || null;
			const apiKey = this.getSetting('server_api_key') || null;
			if (!serverURL || typeof serverURL != 'string' || serverURL.length < 1) return;
			if (!apiKey || typeof apiKey != 'string' || apiKey.length < 1) return;
			const slash = serverURL.endsWith('/') ? '' : '/';
			const endpoint = `${serverURL}${slash}api/notification-count`;
			fetch(endpoint, {
				method: 'GET',
				headers: {
					'Content-Type': 'application/json',
					'Authorization': `Bearer ${apiKey}`
				}
			}).then(response => {
				if (!response.ok) {
					throw new Error(`Server responded with status ${response.status}`);
				}
				return response.json();
			}).then(data => {
				if (!data || typeof data != 'object') {
					throw new Error('Invalid response data');
				}
				if (!data.ok) {
					throw new Error('Failed to fetch notification count');
				}
				const count = data?.count || 0;
				$('#notifications_ticker').empty().append(count > 0 ? `${count}` : '');
				if (count > 0) {
					$('#notifications_opener').addClass('notification-unread');
					$('#notifications_opener').attr('title', `You have ${count} unread notification${count != 1 ? 's' : ''}. Click to view.`);
				} else {
					$('#notifications_opener').removeClass('notification-unread');
					$('#notifications_opener').attr('title', 'No unread notifications.');
				}
			}).catch(error => {
				console.error('Error fetching notification count:', error);
			});
		} catch (e) {
			console.error('Error fetching notification count:', e);
		}
	}

	loadState() {
		chrome.storage.local.get(['settings', 'thread_sort_mode', 'last_channel_id'], (result) => {
			if (chrome.runtime.lastError) {
				console.error('Error loading state:', chrome.runtime.lastError);
				return;
			}
			this.state.settings = result.settings || {};
			this.state.thread_sort_mode = result.thread_sort_mode || 'date_desc'; // default to newest sort mode
			this.state.last_channel_id = result.last_channel_id || null;
			// Ensure all default settings are present if missing
			for (let key in this.settingsDefault) {
				if (!(key in this.state.settings)) {
					this.state.settings[key] = this.settingsDefault[key];
				}
			}
			const apiKey = this.getSetting('server_api_key') || null;
			if(apiKey && typeof apiKey == 'string' && apiKey.length > 0) {
				localStorage.setItem('jwt', apiKey); // Used by render_threads.js and follow.js
			}
			this.authenticate();
		});
	}

	saveState() {
		chrome.storage.local.set({
			settings: this.state.settings || {},
			thread_sort_mode: this.state.thread_sort_mode || 'date_desc',
			last_channel_id: this.state.last_channel_id || null,
		}, () => {
			if (chrome.runtime.lastError) {
				console.error('Error saving state:', chrome.runtime.lastError);
			}
		});
	}

	applyFontSizeSetting() {
		const font_size = this.getSetting('font_size');
		if (!font_size || typeof font_size != 'string' || font_size.length < 1) return;
		// validate that the font size ends with em and is a number from 0.5 to 1.5
		const font_size_num = parseFloat(font_size.replace('em', ''));
		if (isNaN(font_size_num) || font_size_num < 0.5 || font_size_num > 1.5 || !font_size.endsWith('em')) return;
		$('body').css({ fontSize: font_size });
	}

	getSetting(key) {
		if ('settings' in this.state && this.state.settings && typeof this.state.settings == 'object' && key in this.state.settings) {
			return this.state.settings[key];
		}
		return this.settingsDefault[key] || null;
	}

	updateSettings(newSettings) {
		let validSettings = {};
		let invalidParams = [];
		for (let key in newSettings) {
			if (this.settingsSchema[key]) {
				if (typeof newSettings[key] === this.settingsSchema[key]) {
					validSettings[key] = newSettings[key];
				} else if (this.settingsSchema[key] === 'number' && !isNaN(newSettings[key] * 1)) {
					validSettings[key] = newSettings[key] * 1;
				} else if (this.settingsSchema[key] === 'boolean' && ["true", "false"].indexOf(newSettings[key].toString().toLowerCase()) > -1) {
					validSettings[key] = newSettings[key].toString().toLowerCase() === 'true' ? true : false;
				} else {
					invalidParams.push(key);
				}
			} else {
				invalidParams.push(key);
			}
		}

		if (invalidParams.length > 0) {
			const invStr = invalidParams.join(', ');
			this.feed(`Invalid setting or type for parameter(s): ${invStr}`, true);
		} else {
			// merge partial settings with existing settings
			this.state.settings = { ...this.state.settings, ...validSettings };
			this.saveState();
			this.feed("Settings updated.")
		}
	}

	buildSettingsForm() {
		this.emptyContainers();
		this.unloadThread();
		$('#reply-container').empty().append('<h2>Extension Settings</h2>');

		// Create cancel button
		const cancelIcon = bsi('x-lg') || '❌';
		const cancelLink = $(`<a href="#" class="btn btn-sm btn-secondary float-end" id="exit_settings">${cancelIcon} Cancel</a>`);
		cancelLink.on('click', e => {
			e.preventDefault();
			this.getThreads();
		});
		$('#reply-container').append(cancelLink, '<br><br>');

		const settingsForm = $(`<form></form>`);
		for (var key in this.settingsDefault) {
			var input = null;
			var label = `<label for="${key}" title="${(this.settingsDescriptions[key] || '')}">${key.replace(/_/g, ' ').toUpperCase()}</label>`;
			if (key == 'font_size') { // font size dropdown
				input = $(`<select name="font_size"></select>`);
				const options = ['0.5em', '0.6em', '0.7em', '0.8em', '0.9em', '1em', '1.1em', '1.2em', '1.3em', '1.4em', '1.5em'];
				const dflt = this.state.settings?.[key] || this.settingsDefault?.[key] || null;
				for (var j = 0; j < options.length; j++) {
					const opt = options[j];
					const selected = dflt == opt ? ' selected' : '';
					input.append(`<option value="${opt}"${selected}>${opt}</option>`);
				}
			} else {
				const typ = typeof this.settingsDefault[key] === 'number' ? 'number' : 'text';
				const val = this.state.settings?.[key] || this.settingsDefault[key];
				input = $(`<input type="${typ}" title="${(this.settingsDescriptions[key] || '')}" name="${key}" value="${val}">`);
			}
			if (!input) continue; // skip if no input
			if (label) settingsForm.append(label, '<br>');
			input.addClass('form-control');
			settingsForm.append(input, '<br>');
		}

		// append submit button
		settingsForm.append(`<br><br><input type="submit" value="Save Settings" class="btn-primary form-control"><br><br>`);
		settingsForm.on('submit', (e) => {
			e.preventDefault();
			for (var key in this.settingsDefault) {
				const input = settingsForm.find(`[name="${key}"]`);
				if (input.length < 1) continue;
				const val = input.val();
				if (Array.isArray(this.settingsDefault[key])) {
					const checkedBoxes = input.find('input[type="checkbox"]:checked');
					const checkedValues = [];
					checkedBoxes.each((i, el) => {
						checkedValues.push($(el).val());
					});
					this.updateSettings({ [key]: checkedValues });
				} else if (this.settingsSchema?.[key] == 'boolean') {
					this.updateSettings({ [key]: input.is(':checked') });
				} else {
					this.updateSettings({ [key]: val });
				}
			}
			this.saveState();
			this.applyFontSizeSetting();
			this.loadState(); // re-authenticate if api key or server url changed
			$('#exit_settings').trigger('click');
		});
		$('#reply-container').append(settingsForm);

		// Display extension version info
		if (this.version && typeof this.version == 'string') {
			$('#reply-container').append(`<div class="text-muted small" style="margin-top:10px;">Extension Version: ${this.version}</div>`);
		}else{
			$('#reply-container').append(`<div class="text-muted small" style="margin-top:10px;">Extension Version: Unknown</div>`);
		}
	}
}
